// Copyright Epic Games, Inc. All Rights Reserved.

#pragma once

#include <zencore/zencore.h>

#if ZEN_PLATFORM_WINDOWS

ZEN_THIRD_PARTY_INCLUDES_START

struct IUnknown;  // Workaround for "combaseapi.h(229): error C2187: syntax error: 'identifier' was unexpected here" when using /permissive-
#	ifndef NOMINMAX
#		define NOMINMAX  // We don't want your min/max macros
#	endif
#	ifndef NOGDI
#		define NOGDI  // We don't want your GetObject define
#	endif
#	ifndef WIN32_LEAN_AND_MEAN
#		define WIN32_LEAN_AND_MEAN
#	endif
#	ifndef _WIN32_WINNT
#		define _WIN32_WINNT 0x0A00
#	endif
#	include <windows.h>
#	undef GetObject
#	undef SendMessage
#	undef CopyFile

ZEN_THIRD_PARTY_INCLUDES_END

//////////////////////////////////////////////////////////////////////////

namespace zen::windows {

class Handle
{
public:
	Handle() noexcept = default;
	inline Handle(Handle& h) noexcept { Attach(h.Detach()); }
	explicit Handle(HANDLE h) noexcept : m_Handle(h) {}
	inline ~Handle() noexcept
	{
		if (m_Handle)
		{
			Close();
		}
	}

	Handle& operator=(Handle& InHandle) noexcept
	{
		if (this != &InHandle)
		{
			if (m_Handle != NULL)
			{
				Close();
			}
			Attach(InHandle.Detach());
		}

		return *this;
	}

	inline		operator HANDLE() const noexcept { return m_Handle; }
	inline void Attach(HANDLE h) noexcept
	{
		ZEN_ASSERT(m_Handle == NULL);
		m_Handle = h;
	}
	inline HANDLE Detach() noexcept
	{
		HANDLE h;

		h		 = m_Handle;
		m_Handle = NULL;

		return h;
	}
	void Close() noexcept
	{
		if (m_Handle != NULL)
		{
			::CloseHandle(m_Handle);
			m_Handle = NULL;
		}
	}

public:
	HANDLE m_Handle{0};
};

/////////////////////////////////////////////////////////////////////////////
// Error to HRESULT helpers

inline HRESULT
MapHresultFromLastError() noexcept
{
	DWORD ErrorCode = ::GetLastError();
	return HRESULT_FROM_WIN32(ErrorCode);
}

inline HRESULT
MapHresultFromWin32(DWORD ErrorCode) noexcept
{
	return HRESULT_FROM_WIN32(ErrorCode);
}

//////////////////////////////////////////////////////////////////////////

class FileHandle : public Handle
{
public:
	FileHandle() noexcept = default;
	FileHandle(FileHandle& InFile) noexcept : Handle(InFile) {}
	explicit FileHandle(HANDLE InFileHandle) noexcept : Handle(InFileHandle) {}

	HRESULT Create(LPCTSTR szFilename,
				   DWORD   dwDesiredAccess,
				   DWORD   dwShareMode,
				   DWORD   dwCreationDisposition,
				   DWORD   dwFlagsAndAttributes = FILE_ATTRIBUTE_NORMAL)
	{
		ZEN_ASSERT(m_Handle == NULL);

		HANDLE hFile = ::CreateFile(szFilename, dwDesiredAccess, dwShareMode, NULL, dwCreationDisposition, dwFlagsAndAttributes, NULL);

		if (hFile == INVALID_HANDLE_VALUE)
			return MapHresultFromLastError();

		Attach(hFile);

		return S_OK;
	}

	HRESULT Read(LPVOID pBuffer, DWORD nBufSize)
	{
		ZEN_ASSERT(m_Handle != NULL);

		DWORD nBytesRead = 0;
		BOOL  Success	 = ::ReadFile(m_Handle, pBuffer, nBufSize, &nBytesRead, NULL);

		if (!Success)
			return MapHresultFromLastError();

		if (nBytesRead != nBufSize)
			return HRESULT_FROM_WIN32(ERROR_HANDLE_EOF);

		return S_OK;
	}

	HRESULT Read(LPVOID pBuffer, DWORD nBufSize, LPOVERLAPPED pOverlapped)
	{
		ZEN_ASSERT(m_Handle != NULL);

		BOOL Success = ::ReadFile(m_Handle, pBuffer, nBufSize, NULL, pOverlapped);
		if (!Success)
			return MapHresultFromLastError();

		return S_OK;
	}

	HRESULT Write(LPCVOID pBuffer, DWORD nBufSize, DWORD* pnBytesWritten = NULL)
	{
		ZEN_ASSERT(m_Handle != NULL);

		DWORD nBytesWritten;
		if (pnBytesWritten == NULL)
			pnBytesWritten = &nBytesWritten;

		BOOL Success = ::WriteFile(m_Handle, pBuffer, nBufSize, pnBytesWritten, NULL);
		if (!Success)
			return MapHresultFromLastError();

		return S_OK;
	}

	HRESULT Write(LPCVOID pBuffer, DWORD nBufSize, LPOVERLAPPED pOverlapped)
	{
		ZEN_ASSERT(m_Handle != NULL);

		BOOL Success = ::WriteFile(m_Handle, pBuffer, nBufSize, NULL, pOverlapped);
		if (!Success)
			return MapHresultFromLastError();

		return S_OK;
	}

	HRESULT Seek(LONGLONG nOffset, DWORD dwFrom = FILE_CURRENT)
	{
		ZEN_ASSERT(m_Handle != NULL);
		ZEN_ASSERT(dwFrom == FILE_BEGIN || dwFrom == FILE_END || dwFrom == FILE_CURRENT);

		LARGE_INTEGER liOffset;
		liOffset.QuadPart = nOffset;
		DWORD nNewPos	  = ::SetFilePointer(m_Handle, liOffset.LowPart, &liOffset.HighPart, dwFrom);

		if (nNewPos == INVALID_SET_FILE_POINTER)
		{
			HRESULT hr = MapHresultFromLastError();

			if (FAILED(hr))
				return hr;
		}

		return S_OK;
	}

	HRESULT GetPosition(ULONGLONG& OutPos) const
	{
		ZEN_ASSERT(m_Handle != NULL);

		LARGE_INTEGER liOffset;
		liOffset.QuadPart = 0;
		liOffset.LowPart  = ::SetFilePointer(m_Handle, 0, &liOffset.HighPart, FILE_CURRENT);
		if (liOffset.LowPart == INVALID_SET_FILE_POINTER)
		{
			HRESULT hr = MapHresultFromLastError();

			if (FAILED(hr))
				return hr;
		}
		OutPos = liOffset.QuadPart;

		return S_OK;
	}

	HRESULT GetSize(ULONGLONG& OutLen) const
	{
		ZEN_ASSERT(m_Handle != NULL);

		ULARGE_INTEGER liFileSize;
		liFileSize.LowPart = ::GetFileSize(m_Handle, &liFileSize.HighPart);

		if (liFileSize.LowPart == INVALID_FILE_SIZE)
		{
			HRESULT hr = MapHresultFromLastError();
			if (FAILED(hr))
				return hr;
		}

		OutLen = liFileSize.QuadPart;

		return S_OK;
	}
};

//////////////////////////////////////////////////////////////////////////

class FileMapping
{
public:
	FileMapping() throw()
	{
		m_pData	   = NULL;
		m_hMapping = NULL;
	}

	~FileMapping() noexcept(true) { Unmap(); }

	HRESULT MapFile(_In_ HANDLE	   hFile,
					_In_ SIZE_T	   nMappingSize		   = 0,
					_In_ ULONGLONG nOffset			   = 0,
					_In_ DWORD	   dwMappingProtection = PAGE_READONLY,
					_In_ DWORD	   dwViewDesiredAccess = FILE_MAP_READ) throw()
	{
		ZEN_ASSERT(m_pData == NULL);
		ZEN_ASSERT(m_hMapping == NULL);
		ZEN_ASSERT(hFile != INVALID_HANDLE_VALUE && hFile != NULL);

		ULARGE_INTEGER liFileSize;
		liFileSize.LowPart = ::GetFileSize(hFile, &liFileSize.HighPart);
		if (liFileSize.QuadPart < nMappingSize)
			liFileSize.QuadPart = nMappingSize;

		m_hMapping = ::CreateFileMapping(hFile, NULL, dwMappingProtection, liFileSize.HighPart, liFileSize.LowPart, 0);
		if (m_hMapping == NULL)
			return MapHresultFromLastError();

		if (nMappingSize == 0)
			m_nMappingSize = (SIZE_T)(liFileSize.QuadPart - nOffset);
		else
			m_nMappingSize = nMappingSize;

		m_dwViewDesiredAccess = dwViewDesiredAccess;
		m_nOffset.QuadPart	  = nOffset;

		m_pData = ::MapViewOfFileEx(m_hMapping, m_dwViewDesiredAccess, m_nOffset.HighPart, m_nOffset.LowPart, m_nMappingSize, NULL);
		if (m_pData == NULL)
		{
			HRESULT hr;

			hr = MapHresultFromLastError();
			::CloseHandle(m_hMapping);
			m_hMapping = NULL;
			return hr;
		}

		return S_OK;
	}

	HRESULT MapSharedMem(_In_ SIZE_T					nMappingSize,
						 _In_z_ LPCTSTR					szName,
						 _Out_opt_ BOOL*				pbAlreadyExisted	= NULL,
						 _In_opt_ LPSECURITY_ATTRIBUTES lpsa				= NULL,
						 _In_ DWORD						dwMappingProtection = PAGE_READWRITE,
						 _In_ DWORD						dwViewDesiredAccess = FILE_MAP_ALL_ACCESS) throw()
	{
		ZEN_ASSERT(m_pData == NULL);
		ZEN_ASSERT(m_hMapping == NULL);
		ZEN_ASSERT(nMappingSize > 0);
		ZEN_ASSERT(szName != NULL);	 // if you just want a regular chunk of memory, use a heap allocator

		m_nMappingSize = nMappingSize;

		ULARGE_INTEGER nSize;
		nSize.QuadPart = nMappingSize;
		m_hMapping	   = ::CreateFileMapping(INVALID_HANDLE_VALUE, lpsa, dwMappingProtection, nSize.HighPart, nSize.LowPart, szName);
		if (m_hMapping == NULL)
		{
			HRESULT hr = MapHresultFromLastError();
			_Analysis_assume_(FAILED(hr));
			return hr;
		}

		if (pbAlreadyExisted != NULL)
			*pbAlreadyExisted = (GetLastError() == ERROR_ALREADY_EXISTS);

		m_dwViewDesiredAccess = dwViewDesiredAccess;
		m_nOffset.QuadPart	  = 0;

		m_pData = ::MapViewOfFileEx(m_hMapping, m_dwViewDesiredAccess, m_nOffset.HighPart, m_nOffset.LowPart, m_nMappingSize, NULL);
		if (m_pData == NULL)
		{
			HRESULT hr;

			hr = MapHresultFromLastError();
			::CloseHandle(m_hMapping);
			m_hMapping = NULL;
			return hr;
		}

		return S_OK;
	}

	HRESULT OpenMapping(_In_z_ LPCTSTR szName,
						_In_ SIZE_T	   nMappingSize,
						_In_ ULONGLONG nOffset			   = 0,
						_In_ DWORD	   dwViewDesiredAccess = FILE_MAP_ALL_ACCESS) throw()
	{
		ZEN_ASSERT(m_pData == NULL);
		ZEN_ASSERT(m_hMapping == NULL);
		ZEN_ASSERT(szName != NULL);	 // if you just want a regular chunk of memory, use a heap allocator

		m_nMappingSize		  = nMappingSize;
		m_dwViewDesiredAccess = dwViewDesiredAccess;

		m_hMapping = ::OpenFileMapping(m_dwViewDesiredAccess, FALSE, szName);
		if (m_hMapping == NULL)
			return MapHresultFromLastError();

		m_dwViewDesiredAccess = dwViewDesiredAccess;
		m_nOffset.QuadPart	  = nOffset;

		m_pData = ::MapViewOfFileEx(m_hMapping, m_dwViewDesiredAccess, m_nOffset.HighPart, m_nOffset.LowPart, m_nMappingSize, NULL);
		if (m_pData == NULL)
		{
			HRESULT hr;

			hr = MapHresultFromLastError();
			::CloseHandle(m_hMapping);
			m_hMapping = NULL;
			return hr;
		}

		return S_OK;
	}

	HRESULT Unmap() noexcept(true)
	{
		HRESULT hr = S_OK;

		if (m_pData != NULL)
		{
			if (!::UnmapViewOfFile(m_pData))
				hr = MapHresultFromLastError();
			m_pData = NULL;
		}
		if (m_hMapping != NULL)
		{
			if (!::CloseHandle(m_hMapping) && SUCCEEDED(hr))
				hr = MapHresultFromLastError();
			m_hMapping = NULL;
		}
		return hr;
	}

	void* GetData() const throw() { return m_pData; }

	HANDLE GetHandle() const throw() { return m_hMapping; }

	SIZE_T GetMappingSize() throw() { return m_nMappingSize; }

	HRESULT CopyFrom(_In_ FileMapping& orig) throw();

	FileMapping(_In_ FileMapping& orig);
	FileMapping& operator=(_In_ FileMapping& orig);

private:
	void*		   m_pData;
	SIZE_T		   m_nMappingSize;
	HANDLE		   m_hMapping;
	ULARGE_INTEGER m_nOffset;
	DWORD		   m_dwViewDesiredAccess;
};

bool IsRunningOnWine();

}  // namespace zen::windows
#endif
